package com.practice.nio;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Iterator;
import java.util.Scanner;

/**
 * NIO client.
 */
public class NIOClient {

	/**
	 * Selector of SocketChannel.
	 */
	private Selector selector;

	private boolean stop;

	/**
	 * Initializes client by specified server IP and port.
	 * 
	 * @param ip
	 * @param port
	 * @throws IOException
	 */
	public void initClient(String ip, int port) throws IOException {
		// Creates a SocketChannel.
		SocketChannel channel = SocketChannel.open();
		// Sets the SocketChannel to be non-blocking.
		channel.configureBlocking(false);
		// Creates a new selector.
		this.selector = Selector.open();

		// Connect server.
		channel.connect(new InetSocketAddress(ip, port));
		// Registers SocketChannel with selector. OP_CONNECT specifies that we
		// want to listen for connect events.
		channel.register(selector, SelectionKey.OP_CONNECT);

		stop = false;
	}

	/**
	 * Listen for selector, send and receive message.
	 * 
	 * @throws IOException
	 */
	public void listen() throws IOException {
		// Main loop, check selector.
		while (!stop) {
			// This method blocks until at least one of the registered events occurs.
			selector.select();
			// Gets a Set of the SelectionKey objects for which events have occurred.
			Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
			while (ite.hasNext()) {
				SelectionKey key = ite.next();
				// Removes the processed SelectionKey from the set of selected keys.
				ite.remove();

				if (key.isConnectable()) { // It's "connect" event of SocketChannel.
					SocketChannel channel = (SocketChannel) key.channel();
					// Finish connection when it is pending.
					if (channel.isConnectionPending()) {
						channel.finishConnect();

					}
					// Sets the SocketChannel to be non-blocking.
					channel.configureBlocking(false);

					send(channel);
					// Registers SocketChannel of connection with selector.
					channel.register(this.selector, SelectionKey.OP_READ);

				} else if (key.isReadable()) { // It's "read" event of SockectChannel.
					// Get socket channel from key.
					SocketChannel channel = (SocketChannel) key.channel();
					receive(channel);
				}

			}

		}
	}

	/**
	 * Reads message from console and sends to server.
	 * 
	 * @param channel
	 * @throws IOException
	 */
	public void send(SocketChannel channel) throws IOException {
		Scanner scanner = new Scanner(System.in);
		String message = scanner.nextLine();
		scanner.close();

		ByteBuffer byteBuffer = ByteBuffer.allocate(0x400);
		try (ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes());
				ReadableByteChannel consoleChannel = Channels.newChannel(inputStream);) {
			while (true) {
				byteBuffer.clear();
				int readCount = consoleChannel.read(byteBuffer);
				if (readCount < 0) {
					break;
				}

				byteBuffer.flip();
				channel.write(byteBuffer);
			}
		}

	}

	/**
	 * Receives message from server and writes to console.
	 * 
	 * @param channel
	 * @throws IOException
	 */
	public void receive(SocketChannel channel) throws IOException {
		ByteBuffer byteBuffer = ByteBuffer.allocate(0x400);
		try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
				WritableByteChannel consoleChannel = Channels.newChannel(outputStream);) {
			while (true) {
				byteBuffer.clear();
				int readCount = channel.read(byteBuffer);
				if (readCount <= 0) {
					break;
				}

				byteBuffer.flip();
				consoleChannel.write(byteBuffer);
			}

			String message = new String(outputStream.toByteArray());
			System.out.println(message);
		}
	}

	/**
	 * Entry of NIO client.
	 * 
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		NIOClient client = new NIOClient();
		client.initClient("localhost", 8000);
		client.listen();
	}

}